package com.lalilu.lmedia.indexer

import android.annotation.SuppressLint
import android.content.Context
import android.database.ContentObserver
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import com.blankj.utilcode.util.FileUtils
import com.blankj.utilcode.util.LogUtils
import com.lalilu.lmedia.LMedia
import com.lalilu.lmedia.entity.*
import com.lalilu.lmedia.extension.PermissionUtils
import com.lalilu.lmedia.extension.Pipe
import com.lalilu.lmedia.extension.Slot
import com.lalilu.lmedia.extension.connect
import com.lalilu.lmedia.extension.debounce
import com.lalilu.lmedia.scanner.Api21MediaStoreScanner
import com.lalilu.lmedia.scanner.Api29MediaStoreScanner
import com.lalilu.lmedia.scanner.Api30MediaStoreScanner
import com.lalilu.lmedia.scanner.MediaStoreScanner
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.coroutines.CoroutineContext

/**
 * 歌曲信息索引构建器
 */
@OptIn(FlowPreview::class)
object Indexer : ContentObserver(Handler(Looper.getMainLooper())), CoroutineScope {
    override val coroutineContext: CoroutineContext = Dispatchers.IO
    private val scanner by lazy {
        @SuppressLint("ObsoleteSdkInt")
        when {
            Build.VERSION.SDK_INT >= 30 -> Api30MediaStoreScanner()
            Build.VERSION.SDK_INT >= 29 -> Api29MediaStoreScanner()
            Build.VERSION.SDK_INT >= 21 -> Api21MediaStoreScanner()
            else -> MediaStoreScanner()
        }
    }
    lateinit var getContext: () -> Context
    private val indexEvent = MutableStateFlow(System.currentTimeMillis())
    private val filterSlot = Slot(Pipe<Long, List<FilterType>> { flowOf(listOf(FilterType.None)) })
    private var flowJob: Job? = null

    fun preInit(getContext: () -> Context) {
        this.getContext = getContext
    }

    fun startListen() {
        getContext().applicationContext.contentResolver
            .unregisterContentObserver(this)
        flowJob?.cancel()

        getContext().applicationContext.contentResolver
            .registerContentObserver(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                true,
                this
            )
        flowJob = indexEvent.connect(filterSlot)
            .debounce(200)
            .onEach { startIndex(it) }
            .launchIn(this)
    }

    private val tryIndex = debounce(2000) {
        indexEvent.emit(System.currentTimeMillis())
    }

    fun setFilterPipe(pipe: Pipe<Long, List<FilterType>>) {
        filterSlot.setPipe(pipe)
    }

    override fun onChange(selfChange: Boolean, uri: Uri?) {
        LogUtils.i("[Indexer]onChange, selfChange:${selfChange}, uri:${uri}")
        tryIndex.invoke()
    }

    private suspend fun startIndex(filterTypes: List<FilterType>) =
        withContext(Dispatchers.IO) {
            LogUtils.i("[Indexer]start index")

            val context = getContext().applicationContext

            if (!PermissionUtils.check(context)) {
                LogUtils.i("[LMedia]: ${PermissionUtils.permission} notGrant")
                return@withContext
            }

            val start = System.currentTimeMillis()

            val buildSongJob = async {
                buildSongs(filterTypes).apply {
                    LogUtils.i("Build Songs Cost: ${System.currentTimeMillis() - start} songs size: $size")
                    LMedia.songsFlowCache = this
                    LMedia.updateState(LMedia.State.STATE_READY)
                }
            }
            val preBuildGenreJob = async {
                preBuildGenres().apply {
                    LogUtils.i("Init Genres Cost: ${System.currentTimeMillis() - start} genres size: $size")
                }
            }
            val linkGenreJob = async {
                val genres = preBuildGenreJob.await()
                val songs = buildSongJob.await()
                linkGenres(genres, songs)
            }

            val buildAlbumJob = async {
                val songs = buildSongJob.await()
                buildAlbums(songs.values, filterTypes).apply {
                    LogUtils.i("Build Albums Cost: ${System.currentTimeMillis() - start} albums size: $size")
                    LMedia.albumsFlowCache = this
                }
            }

            val buildArtistJob = async {
                val songs = buildSongJob.await()
                buildArtists(songs.values, filterTypes).apply {
                    LogUtils.i("Build Artists Cost: ${System.currentTimeMillis() - start} artists size: $size")
                    LMedia.artistsFlowCache = this
                }
            }

            val buildGenreJob = async {
                linkGenreJob.await()
                val songs = buildSongJob.await()
                buildGenres(songs.values, filterTypes).apply {
                    LogUtils.i("Build Genres Cost: ${System.currentTimeMillis() - start} genres size: $size")
                    LMedia.genresFlowCache = this
                }
            }

            val buildDictionaryJob = async {
                val songs = buildSongJob.await()
                buildDictionaries(songs.values, filterTypes).apply {
                    LogUtils.i("Build Dictionary Cost: ${System.currentTimeMillis() - start} dictionary size: $size")
                    LMedia.dictionariesFlowCache = this
                }
            }

            listOf(
                buildSongJob,
                preBuildGenreJob,
                linkGenreJob,
                buildAlbumJob,
                buildArtistJob,
                buildGenreJob,
                buildDictionaryJob
            ).joinAll().apply {
                LogUtils.i("Build Finished Cost: ${System.currentTimeMillis() - start}")
            }
            return@withContext
        }

    private fun preBuildGenres(): List<Pair<Long, String>> {
        return scanner.initGenres(getContext().applicationContext)
    }

    private fun linkGenres(genres: List<Pair<Long, String>>, songs: Map<String, LSong>) {
        scanner.linkGenres(getContext().applicationContext, genres, songs.values)
    }

    private suspend fun buildSongs(filterTypes: List<FilterType>): Map<String, LSong> {
        return scanner.buildSongs(getContext().applicationContext)
            .filterBy(filterTypes)
            .associateBy { it.id }
    }

    private fun buildAlbums(
        songs: Collection<LSong>,
        filterTypes: List<FilterType>
    ): Map<String, LAlbum> {
        return songs
            .groupBy { it._albumId }
            .map {
                LAlbum(
                    id = it.key,
                    name = it.value[0]._albumTitle,
                    artistName = it.value[0]._albumArtist,
                    songs = it.value
                )
            }
            .merge()    // 将具有相同名称的LAlbum进行合并
            .filterBy(filterTypes)
            .link()
            .markBlockedByItems()
            .associateBy { it.id }
            .toMutableMap()
    }

    private fun buildArtists(
        songs: Collection<LSong>,
        filterTypes: List<FilterType>
    ): Map<String, LArtist> {
        /**
         * 先根据MediaStore分配的原始[LSong._artistId]ID进行分组，
         * 目的是将歌曲进行划分
         */
        return songs
            .groupBy { it._artist }
            .map {
                LArtist(
                    name = it.key,
                    songs = it.value
                )
            }
            .separate()     // separate() 将LArtist拆分成多个LArtist
            .merge()        // merge() 将具有相同name的LArtist合并为同一个
            .filterBy(filterTypes)
            .link()
            .markBlockedByItems()
            // 不根据 ID 构建索引，artistId 现在没多大用处，使用artist的name作为索引可以方便获取歌曲
            .associateBy { it.name }
            .toMutableMap()
    }

    private fun buildGenres(
        songs: Collection<LSong>,
        filterTypes: List<FilterType>
    ): Map<String, LGenre> {
        return songs
            .groupBy { it.genreId ?: "-1" }
            .map {
                LGenre(
                    id = it.key,
                    name = it.value[0].genreName ?: "Uncategorized",
                    songs = it.value
                )
            }
            .filterBy(filterTypes)
            .link()
            .markBlockedByItems()
            .associateBy { it.id }
            .toMutableMap()
    }

    private fun buildDictionaries(
        songs: Collection<LSong>,
        filterTypes: List<FilterType>
    ): Map<String, LDictionary> {
        return songs
            .groupBy {
                FileUtils.getDirName(it.pathStr)
                    ?.takeIf(String::isNotEmpty)
                    ?: "Unknown dir"
            }
            .map {
                LDictionary(
                    path = it.key,
                    songs = it.value
                )
            }
            .filterBy(filterTypes)
            .link()
            .markBlockedByItems()
            .associateBy { it.id }
            .toMutableMap()
    }
}